/*
 * Licensed Materials - Property of tenxcloud.com
 * (C) Copyright 2018 TenxCloud. All Rights Reserved.
 * 2018-06-13  @author lizhen
 */

package jwt

import (
	"errors"
	"github.com/dgrijalva/jwt-go"
	"strings"
	"time"
)

var (
	UnexpectedSingingMethod = errors.New("unexpected signing method")
	UnknownEntity           = errors.New("unknown entity")
)

func NewJWTAuthorizer(c config, cryptor cryptor) *authorizer {
	tl := time.Duration(c.JWTTokenLife())
	return &authorizer{
		tokenLifeInHour:   tl,
		tokenLifeInSecond: int(time.Hour * tl / time.Second),
		issuer:            c.JWTIssuer(),
		cert:              c.JWTCert(),
		cryptor:           cryptor,
	}
}

type config interface {
	JWTCert() []byte
	JWTTokenLife() int
	JWTIssuer() string
}

type loginInfo interface {
	GetUserID() int
	GetUserName() string
	GetUserToken() string
}

type authorizer struct {
	tokenLifeInHour   time.Duration
	tokenLifeInSecond int
	issuer            string
	cert              []byte
	cryptor           cryptor
}

type entity struct {
	Encrypted          string `json:"encrypted"`
	jwt.StandardClaims `json:",inline"`
}

func (a authorizer) Authorize(li loginInfo) (token string, err error) {
	i := &info{
		Username: li.GetUserName(),
		Token:    li.GetUserToken(),
		UserID:   li.GetUserID(),
	}
	now := time.Now()
	var encrypted string
	if encrypted, err = encryptLoginInfo(i, a.cryptor); err != nil {
		return
	}
	claims := jwt.NewWithClaims(jwt.SigningMethodHS512, &entity{
		Encrypted: encrypted,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: now.Add(a.tokenLifeInHour * time.Hour).Unix(),
			Issuer:    a.issuer,
		},
	})
	var rawToken string
	if rawToken, err = claims.SignedString(a.cert); err != nil {
		return
	}
	token = (&dto{
		Token:     rawToken,
		ExpiresIn: a.tokenLifeInSecond,
		IssuedAt:  now,
		UserID:    i.UserID,
	}).String()
	return
}

func (a authorizer) Validate(token string) (l loginInfo, err error) {
	var t *jwt.Token
	if t, err = jwt.ParseWithClaims(token, &entity{}, a.validateCert); err != nil {
		return
	}
	obj, ok := t.Claims.(*entity)
	if !ok {
		err = UnknownEntity
		return
	}
	var li *info
	if li, err = decryptLoginInfo(obj.Encrypted, a.cryptor); err != nil {
		return
	}
	l = li
	return
}

const (
	bearerKeyword       = "Bearer"
	bearerKeywordLength = len(bearerKeyword)
)

func (authorizer) IsBearerToken(rawToken string) (ok bool, token string) {
	ok = len(rawToken) > bearerKeywordLength &&
		strings.EqualFold(bearerKeyword, rawToken[:bearerKeywordLength])
	if ok {
		token = strings.TrimSpace(rawToken[bearerKeywordLength:])
	}
	return
}

func (authorizer) IsTokenExpired(err error) bool {
	if jve, ok := err.(*jwt.ValidationError); ok && jve.Inner != nil {
		return jve.Errors&jwt.ValidationErrorExpired == jwt.ValidationErrorExpired
	}
	return false
}

func (a authorizer) validateCert(token *jwt.Token) (obj interface{}, err error) {
	if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
		err = UnexpectedSingingMethod
		return
	}
	return a.cert, nil
}
